 Bonjour à tous, aujourd'hui nous allons voir la suite du cours sur la synchronisation.
 Dans le cours précédent, on avait vu comment résoudre le problème de l'exclusion mutuelle.
 On a vu deux algorithmes, l'algorithme du test ANSET et l'algorithme de Peterson.
 L'inconvénient de ces deux algorithmes, c'est que ça génère une attente active.
 On est obligé de tester en permanence si la ressource est disponible.
 C'est pour ça qu'il y a un outil qui est plus puissant, qui a été introduit par Dijkstra en 1974, qui s'appelle les sémaphores.
 C'est un outil de synchronisation plus général, qui permet de résoudre plusieurs problèmes de synchronisation,
 pas uniquement le problème de l'exclusion mutuelle, comme on va le voir.
 L'avantage des sémaphores, c'est qu'ils évitent les attentes actives.
 On n'est pas obligé en permanence de tester si une ressource est disponible.
 Qu'est-ce qu'un sémaphore ? C'est une entité qui est gérée par le système d'exploitation, qui contient deux champs.
 Il y a un compteur. Quand vous définissez un sémaphore, vous définissez un compteur
 qui correspond au nombre de ressources disponibles et une file d'attente.
 Si jamais vous tentez d'accéder à une ressource et qu'il n'y en a plus aucune disponible, c'est une sorte de nombre de jetons,
 à ce moment-là, vous allez vous bloquer, vous allez vous mettre à l'état bloqué,
 et on va vous insérer dans une file d'attente des processus sur ce sémaphore.
 L'accès à un sémaphore S ne se fait pas directement. On ne teste pas directement le compteur ou la file.
 Ce sont des structures de données qui sont opaques, que l'utilisateur ne voit pas.
 On y accède via deux opérations de base, qui sont les opérations P et V.
 L'opération P consiste à demander un accès à la ressource.
 Le moyen mnémotechnique qu'on a en français, c'est de dire "Puis-je y accéder à la ressource ?"
 Historiquement, le terme P vient du hollandais "proberand", qui veut dire "tester",
 parce que Dijkstra était professeur en Hollande.
 L'opération inverse, dans laquelle on va libérer l'accès à une ressource,
 lorsque je fais un P, je vais prendre un des jetons disponibles,
 et l'opération inverse, dans laquelle je vais libérer un des jetons que je possédais, c'est l'opération V.
 On libère l'accès à la ressource, et le moyen mnémotechnique qu'on a en français,
 pour se rappeler la différence entre P et V, c'est le V de "vazi".
 Historiquement, V ne veut pas dire "vazi", mais "zerogun", qui en néerlandais veut dire "incrémenter".
 Donc je re-augmente la valeur du compteur, parce que je libère un accès à la ressource.
 En plus de ces opérations P et V, il y a une troisième opération, qui est le init,
 en TD, qu'on appelle CS, pour créer le sémaphore, mais c'est la même chose.
 En tout, on a trois opérations. Ces trois opérations sont indivisibles,
 elles ne peuvent pas être interrompues par une interruption horloge pendant l'exécution de l'opération.
 Il y a le init, dans lequel on crée le sémaphore sem, et on lui affecte la valeur val.
 On donne au compteur du sémaphore sem la valeur val.
 Il y a l'opération P sur un sémaphore, qui demande l'acquisition d'une ressource,
 qui prend une des ressources disponibles, et si aucune ressource n'est disponible,
 le processus se bloque dans la file d'attente du sémaphore.
 Donc là, on va décrémenter le compteur.
 L'opération inverse, c'est l'opération de libération de la ressource, c'est le vazi,
 dans lequel, si la file d'attente n'est pas vide, on va réveiller un des processus qui est bloqué,
 dans le sémaphore, donc on ne réveille qu'un seul, ça ne réveille pas tous les processus,
 donc ça ne réveille qu'un seul, et ça incrémente le compteur, ça libère,
 ça donne de nouveau un accès à la ressource.
 De manière plus concrète, si on voit le pseudo-code, à quoi ça peut ressembler l'implémentation d'un sémaphore,
 donc là je vous donne le pseudo-code en C, d'un sémaphore,
 donc on peut imaginer que le sémaphore, c'est une structure, que j'ai appelée SEM ici,
 avec le compteur et la liste d'attente, que l'on modélise par une file.
 Donc le unit consiste à initialiser la file à vide,
 et à donner au compteur la valeur val, qui est passée en paramètres.
 Donc au départ, quand vous créez un sémaphore, il faut toujours ouvrir ceci une valeur.
 Remarque importante, lorsque vous créez un sémaphore,
 la valeur val que vous lui attribuez doit être supérieure ou égale à 0.
 Ça n'a pas de sens d'initialiser un sémaphore à une valeur négative.
 Donc attention, dans un unit sémaphore, c'est possible d'initialiser un sémaphore à une valeur nulle,
 c'est-à-dire qu'au départ, il n'y a aucune ressource disponible,
 mais ça n'a pas de sens d'initialiser un sémaphore à une valeur moins 1.
 C'est-à-dire qu'il y a moins une ressource disponible, ça n'a pas de sens.
 Donc attention, je ne veux pas avoir de initialisation négative ici.
 Donc ça c'est limite.
 Ensuite, lorsque je fais un p sur une ressource, je vais prendre une des autorisations.
 Donc je décrémente le compteur, qui représente le nombre d'autorisations.
 Donc on décrémente le compteur.
 Si ce compteur passe en négatif,
 c'est-à-dire qu'il va aller au bout, par exemple, il va aller 0, maintenant il passe à -1.
 À ce moment-là, c'est qu'il n'y a pas de ressource disponible.
 Il faut bloquer le processus appelant.
 Donc le processus appelant, qui est le processus courant, va s'insérer dans la file du sémaphore,
 puis va basculer à l'état bloqué, en appelant une primitive interne qui s'appelle "slip".
 Le "V", c'est l'opération inverse.
 Le "V", on libère un accès à la ressource, donc on réincrémente le compteur,
 pour dire que de nouveau, il y a une ressource de plus qui est disponible.
 Par exemple, le compteur va passer de 9 à 10,
 donc j'ai 10 ressources disponibles.
 Et si une fois que j'ai incrémenté, je me rends compte que mon compteur était négatif,
 par exemple, il va aller -1.
 Donc en l'incrémentant, maintenant que je l'ai incrémenté, il va au 0.
 Ça veut dire qu'il y avait un processus qui s'était bloqué dans la file du sémaphore.
 Donc à ce moment-là, je retire un de ces processus de la file.
 On en retire un seul, donc j'appelle une primitive interne "retirer",
 qui en général va retirer le premier, celui qui est en tête de la file d'attente,
 et le rebascule à l'état "près".
 On voit en "wakeup", l'opération "wakeup" le rebascule au processus à l'état "près".
 Donc là, on voit ici que lorsque le compteur est positif ou nul,
 ça correspond au nombre de processus autorisés à accéder à la ressource.
 C'est le nombre de ressources disponibles.
 Lorsque le compteur est supérieur ou égal à 0.
 Si c'est égal à 0, c'est qu'il n'y en a aucune disponible.
 Si c'est égal à 10, c'est qu'il y en a 10 disponibles.
 On peut être 10 en même temps à accéder à la ressource.
 Par contre, on voit bien que la sémantique change un peu.
 Si le compteur devient négatif, s'il vaut -1, -2, -3,
 en fin de compte, ça veut dire que s'il vaut -1, c'est qu'il y en a un qui s'est bloqué dans la file d'attente.
 S'il vaut -2, c'est qu'il y en a deux qui se sont bloqués sur la file d'attente.
 S'il vaut -3, c'est qu'il y en a trois qui sont bloqués sur la file d'attente.
 Lorsque le compteur est négatif, sa valeur absolue correspond au nombre de processus bloqués dans la file d'attente.
 On a vu le pseudo-code.
 Le plus simple pour illustrer ça, c'est de voir les petits exemples.
 Le premier problème qui nous a intéressé depuis le début de cette synchronisation,
 c'est le problème de l'exécution mutuelle.
 Comment on peut résoudre l'exécution mutuelle avec ce petit outil des sémaphores ?
 Là, ça va être très simple.
 Une exécution mutuelle, c'est-à-dire qu'on ne doit créer qu'un seul processus à la fois qui accède à la ressource critique.
 On n'en autorise qu'un seul à la fois.
 Il suffit de définir un sémaphore dont le compteur va être égal à 1.
 Une seule autorisation.
 Ici, je crée un sémaphore que j'appelle "mutex".
 En général, les sémaphores d'exécution mutuelle, on les appelle "mutex".
 C'est l'abréviation en anglais de "mutual exclusion".
 Je crée un sémaphore "mutex" auquel je lui attribue la valeur 1.
 Je n'autorise qu'un seul à la fois.
 Lorsque j'appelle "entrer en section critique", je fais "Puis-je accéder au mutex ?"
 Je fais un "p mutex".
 Si je suis autorisé à accéder, c'est-à-dire que je ne serai pas bloqué dans le "p", j'accède à la section critique.
 Ensuite, lorsque j'ai terminé d'accéder à ma section critique,
 je libère, j'autorise le suivant à y aller en faisant "v mutex".
 Puis-je accéder à la ressource critique ?
 Vas-y, tu peux accéder. Puis-je ? Vas-y. Puis-je ? Vas-y.
 Pour vous convaincre que ça marche, on va illustrer ça sur un petit exemple.
 Là, j'ai représenté ici en bleu l'état de mon sémaphore "mutex" qu'on a créé initialement.
 On voit bien que lorsque j'ai fait le "init", j'ai créé un sémaphore dont le compteur vaut 1 et la file est vide.
 Donc, elle est illustrée par ces deux symboles.
 Au départ, j'ai trois processus P1, P2, P3 qui veulent tous accéder à la ressource critique.
 Ce qu'on veut éviter, c'est qu'il y ait la section critique de P1 qui soit en parallèle de la section critique de P2, et ainsi de suite.
 On veut que les sections critiques s'exécutent les unes après les autres, qu'elles ne soient jamais en parallèle.
 Chaque processus avant d'accéder à la section critique fait "p mutex".
 Imaginons que le scénario, c'est P1 qui est initialement élu, donc il fait "p mutex".
 On applique l'algorithme de P qu'on vient de voir.
 Je décrémente le compteur, mon compteur passe à 0, et c'est uniquement s'il est négatif que je me bloque.
 Là, 0 ce n'est pas négatif.
 Du coup, je titre la fonction P et je commence la section critique.
 A ce moment-là, si je suis interrompu, si il y a une fin de quantum et que P2 est élu,
 P2 cherche à accéder à la même ressource critique.
 Pareil, il va faire un "p mutex" et on applique le même algorithme.
 Je décrémente la valeur de mon compteur.
 Maintenant, c'est valé 0, la valeur vaut -1.
 -1 est bien strictement inférieur à 0, c'est bien négatif.
 A ce moment-là, mon test est vrai, donc j'insère le processus courant dans la file du sémaphore.
 Donc P2 s'insère dans la file du sémaphore et passe à l'état bloqué en appelant la fonction "sleep".
 Donc là, ici, ce que j'illustrais en pointillé.
 Donc le processus est bloqué, vraiment, il est passé à l'état bloqué.
 Pareil, si je suis interrompu par un processus P3 qui lui, également, tente d'accéder à la ressource critique,
 pareil, il fait "p mutex", on décrémente le compteur qui passe à -2,
 -2 étant négatif, le processus courant s'insère dans la file d'attente et bascule à l'état bloqué.
 Donc là, ce qu'on observe, c'est que la valeur du compteur vaut -2,
 ça veut dire que j'ai deux processus dans la file d'attente.
 Donc là, le système, on n'a pas le choix, seul P1 peut être élu.
 P1, lui, continue sa section critique.
 On voit bien que les sections critiques, les demandes de section critique de P2 et de P3 sont bloquées.
 Donc eux ne peuvent pas accéder à leur section critique, c'est exactement ce qu'on veut.
 Et uniquement lorsque P1 a fini d'accéder à sa section critique,
 lorsqu'il a terminé de manipuler la ressource critique, il appelle "v mutex".
 À ce moment-là, "v mutex" incrémente le compteur, qui va aller -2, et le compteur passe à -1.
 -1, c'est bien inférieur ou égal à 0.
 À ce moment-là, je retire un des processus de ma file d'attente,
 donc je retire P2 de ma file d'attente, et je le rebascule à l'état prêt.
 Donc voilà l'état de mon sémaphore, maintenant je n'ai plus que P3 qui est bloqué, et mon sémaphore qui vaut -1.
 Donc à ce moment-là, la prochaine fin de compteur,
 quand P2 est de nouveau à l'état prêt, il va pouvoir être élu,
 exécuter sa section critique,
 et pareil, lorsqu'il a terminé, il appelle "v mutex".
 Donc là, on incrémente le compteur, qui de -1 passe à 0.
 Donc 0, c'est bien inférieur ou égal à 0.
 Donc là, ça veut dire qu'au début de mon "v", mon compteur vaut -1,
 c'est-à-dire qu'il y en a un qui est bloqué dans la file d'attente.
 Comme je l'incrémente, il passe à 0.
 À ce moment-là, je prends un des processus, le dernier processus de ma file d'attente,
 et je le rebascule à l'état prêt.
 Donc à la fin, mon compteur est à 0,
 et je n'ai plus aucun processus de ma file d'attente.
 Donc au bout d'un certain temps, P3 va être élu,
 il va pouvoir exécuter sa section critique,
 et lorsqu'il a terminé, il appelle "v source sur le sémaphore".
 On incrémente le compteur, qui de 0 passe à 1.
 Lorsqu'il est à 1, ça veut dire qu'il n'y en a aucun qui est dans la file d'attente.
 Donc je ne fais rien de plus.
 Donc si on observe ici ce qui s'est passé,
 on voit bien que les trois processus ont accédé de manière...
 les uns après les autres à la section critique.
 Donc je n'ai jamais eu deux sections critiques en parallèle,
 elles ne se sont jamais mélangées, la cohérence est assurée.
 Et je n'ai pas eu d'attente active.
 Donc là, on voit bien que P2 et P3,
 eux, ont bien basculé à l'état bloqué pendant les sections critiques.
 Donc c'est plus efficace, on perd moins de temps à tester en permanence l'état de la ressource.
 Ce que faisait un PterSom ou un TestN7.
 Donc ça, c'était pour le problème des Mutex.
 Après, il y a plein d'autres problèmes qu'on peut régler avec les sémaphores,
 notamment un autre problème classique, c'est une barrière de synchronisation.
 Donc c'est un problème de synchronisation classique qu'on trouve dans beaucoup de programmes.
 Typiquement, vous avez un ensemble de processus,
 imaginons ici trois processus P1, P2, P3.
 J'ai un processus maître qui doit faire un calcul global,
 qui doit collecter les résultats par des processus qu'on appelle des workers.
 Donc eux, P2, P3 et ainsi de suite font un petit calcul local.
 Lorsqu'ils ont terminé leur calcul local,
 ils doivent le signaler au processus P1,
 qui va collecter les résultats pour faire un calcul global.
 Donc P1 attend que les processus P2 et P3 aient fini leur calcul local
 pour calculer la solution finale.
 C'est ce qu'on appelle une barrière de synchronisation.
 C'est le problème qu'on veut résoudre.
 Comment l'implémenter avec des sémaphores ?
 Là, on voit bien que P1 doit attendre que le processus P2 termine son calcul
 et que le processus P3 termine son calcul.
 La façon la plus simple de gérer ça, c'est d'avoir un sémaphore pour attendre P2
 et un sémaphore pour attendre P3.
 Je définis ici deux sémaphores.
 Le sémaphore S1 pour attendre P2 et le sémaphore S2 pour attendre P3.
 Ensuite, ce sémaphore, on veut que dès le départ, je sois bloqué.
 C'est-à-dire que P1, dès le départ, il attend.
 C'est un sémaphore dans lequel je vais faire P pour me mettre en attente sur ce sémaphore,
 mais il faut que dès le départ, je sois bloqué sur le sémaphore.
 De même pour P2.
 P1 doit attendre P2, il doit attendre P3.
 Donc il doit se bloquer dès le départ en attendant que P2 et P3 finissent leur calcul.
 Dans ce cas-là, on va initialiser le sémaphore non pas A1.
 Si vous initialisez A1 et que vous faites un P sur un sémaphore A1, vous n'êtes pas bloqué,
 comme dans le mutex.
 Le mutex, au départ, tout à l'heure, P1, et vous pouvez y aller.
 Là, c'est parce que je veux dès le départ le bloquer.
 Je vais initialiser mon sémaphore S1 à la valeur 0,
 et mon sémaphore S2 à la valeur 0.
 Ça, on a le droit de le faire.
 Ce que je vous ai dit tout à l'heure, c'est qu'on n'a pas le droit d'initialiser à des valeurs négatives.
 Ça, on n'a pas le droit.
 Ça n'a pas de sens dans le sémaphore de Dichter.
 Par contre, 0, on est amené à le faire si dès le départ, on veut être bloqué.
 C'est exactement ce qu'on veut faire ici.
 Donc là, ici, je veux dès le départ que P1 se mette en attente.
 Donc il va attendre P1, il attend P2, puis il attend P3.
 Donc il fait P2, S1, P2, S2.
 Dès que P2 a terminé de faire son calcul local, il va réveiller P1 en l'usant BASI dans le V2, S1.
 C'est pareil pour P3.
 Dès que P3 a terminé son calcul local, il va réveiller P1 en l'usant BASI dans le V2, S2.
 Pour vous convaincre que ça marche, un petit exemple.
 Il y a différents scénarios possibles.
 Imaginons que le premier scénario, au début, ce soit P1 qui soit élu.
 Qu'est-ce qui se passe à ce moment-là ?
 P1 est élu.
 Il fait un P sur le sémaphore S1 qui, je vous rappelle, est initialisé à 0.
 Donc le sémaphore passe en négatif.
 J'ai illustré les compteurs du sémaphore entrecrochés.
 Le compteur passe en négatif.
 À ce moment-là, dès que vous passez en négatif, vous insérez dans la file du sémaphore et vous passez à l'état bloqué.
 Donc P1 est bien bloqué. Il s'insère dans la file du sémaphore.
 Dès que P2 a terminé son exécution, il va faire un V2, S1.
 Il décrémente la valeur du sémaphore S1, qui passe de -1 à 0
 et réveille l'unique processus qui était en attente dans la file.
 Il rebascule P1 à l'état prêt.
 P1 peut être élu, mais il va tout de suite se rebloquer en faisant un P2, S2.
 Là, c'est le compteur du sémaphore S2 que je décrémente.
 Il va à 0, il va au -1.
 Et pareil, je m'insère dans la file du sémaphore.
 Là, on peut faire également un second scénario.
 Cette fois-ci, P3 va terminer son calcul avant P2.
 Qu'est-ce qui se passe à ce moment-là ?
 P1 est élu, comme tout à l'heure.
 Il fait P2, S1.
 Le sémaphore de S1 est initialisé à 0.
 Il va s'endormir.
 Le compteur passe à -1 et P1 s'insère dans la file du sémaphore S1.
 Dès le départ, P3 termine très rapidement son calcul local.
 Il va faire son P2, S1.
 Il incrémente la valeur du sémaphore S1 qui va aller 0, qui passe à 1.
 Il n'y a aucun point de suite dans la file d'attente, donc il ne fait rien de plus.
 De toute façon, P1 est bloqué tant que P2 n'a pas fait le P2, S1.
 Il reste à l'état bloqué ici.
 Au bout d'un moment, P2 a terminé son calcul local.
 À ce moment-là, il incrémente la valeur du sémaphore S1.
 Il passe de -1 à 0 et réveille le processus P1 qui était en attente.
 Le processus P1 va être élu.
 Il va faire le P2, S2.
 Mais S2 a un compteur égal à 1.
 Il va se contenter de décrémenter la valeur du compteur du sémaphore S2.
 Il va passer de 1 à 0.
 0 n'est pas strictement inférieur à 0, donc il ne va pas se bloquer.
 Il va directement commencer à faire son calcul global.
 Le calcul global va avoir lieu après le calcul local de P2 et le calcul local de P3.
 Maintenant, un autre petit exemple d'un problème classique
 qu'on va étudier en détail en TD, qui est le problème du producteur-consommateur.
 C'est quelque chose qui est très courant en informatique.
 Ici, vous avez des processus producteurs et des processus consommateurs
 qui coopèrent et qui s'échangeent de l'information via un tampon.
 En mémoire, T de taille N, qui peut contenir N cases.
 Le producteur dépose des informations dans le tampon T,
 dans une des cases disponibles, parmi les N disponibles du tampon.
 Le consommateur vide une des cases du tampon et fait un traitement dessus.
 Il dépose des informations dans le tampon et le consommateur retire les informations du tampon.
 Les deux sont de manière complètement désynchronisée.
 Le producteur peut déposer plusieurs cases avant que le consommateur commence à lire les cases du tampon.
 C'est complètement désynchronisé comme information.
 Ce n'est pas comme un emboît de message en réseau.
 On dépose et l'autre retire.
 C'est très classique, on le retrouve à plein d'endroits dans les systèmes.
 Il est déjà manipulé indirectement en shell.
 Lorsque vous faites la commande en shell pipe,
 je fais ls -l pipe worktemp -l pour compter le nombre de fichiers dans mon répertoire,
 c'est une commande classique.
 Ici, on fait le compte, le système d'exploitation implémente un producteur-consommateur.
 Ls -l, c'est le producteur.
 Tous les affichages de ls -l vont être recopiés dans une structure interne, un tampon interne.
 Toutes les informations dans ce tampon interne vont être lues par le processus consommateur ls -w worktemp -m.
 Là, c'est un exemple où deux processus implémentent un producteur-consommateur.
 C'est implémenté comme ça dans la plupart des Unix.
 On veut résoudre ce problème de synchronisation.
 Quand vous avez un problème de synchronisation comme ça à résoudre,
 il faut identifier les contraintes qu'on a.
 Je vous ai dit dès le départ que le tampon n'est pas de taille infinie.
 Il a deux tailles n.
 Chaque producteur-consommateur avance à son propre rythme.
 Je ne sais pas du tout, ça dépend de l'ordonnanceur.
 On ne peut pas savoir qui va être lu d'abord, qui va passer d'abord,
 combien il y a de producteurs, combien il y a de consommateurs.
 Ça, vous ne le savez pas.
 Par contre, vous pouvez savoir si le tampon est plein ou dévidé.
 Il n'y a aucune raison de l'empêcher de produire tant que le tampon n'est pas plein.
 Par contre, dès que le tampon est plein,
 dès que toutes les mécases sont remplies, il faut le bloquer.
 Il ne faut pas qu'il puisse produire dans un tampon plein.
 Sinon, vous allez commencer à perdre de l'information,
 à écraser l'information produite par d'autres producteurs.
 Première contrainte, c'est que le producteur ne peut pas déposer un message
 lorsque le tampon est plein.
 De même, le consommateur,
 imaginons que dès le départ, ce soit le consommateur qui soit élu,
 il ne peut pas retirer le message s'il n'y a rien à lire.
 Pour éviter de lire n'importe quoi ou de lire du vide,
 il faut bloquer le consommateur lorsque le tampon est vide.
 On voit bien qu'il y a deux conditions.
 Bloquer le producteur lorsque le tampon est plein,
 bloquer le consommateur lorsque le tampon est vide.
 À chaque condition de blocage, il va correspondre un sémaphore.
 Bloquer, ça veut dire faire un P sur un sémaphore.
 Il faut raisonner comme ça.
 Une troisième contrainte qui va être importante à résoudre,
 et on verra ça plus en détail en TD,
 c'est que le producteur et le consommateur,
 même s'il y a de la place dans le tampon,
 il faut éviter qu'ils acceptent simultanément à la même case.
 Il ne faut pas que deux producteurs écrivent dans la même case.
 Il ne faut pas que deux consommateurs lisent,
 retirent en même temps dans la même case.
 Voilà les trois contraintes qu'on a résolues
 lorsqu'on a programmé un producteur consommateur.
 À chacune de ces contraintes va correspondre un sémaphore.
 On va avoir le sémaphore S const pour bloquer les consommateurs
 lorsque le tampon est vide.
 Donc on va le bloquer.
 La première chose que vont faire les consommateurs,
 ils vont dire "Puis-je consommer ?"
 Donc initialement, comment savoir quelle valeur on donne à ce sémaphore ?
 Vous ne pouvez pas consommer,
 vu qu'initialement lorsqu'on crée le tampon,
 on le crée vide, on ne crée pas d'informations dedans.
 Donc initialement, vous n'autorisez aucune consommation.
 Ça veut dire que ce sémaphore S const va être initialisé à zéro.
 Vous autorisez zéro consommation initialement.
 Le second sémaphore, c'est le sémaphore qui me sert à bloquer les producteurs.
 Donc initialement, pareil, le tampon est en vide.
 Si les producteurs arrivent, vous autorisez N production dès le départ.
 Vous pouvez déposer N messages avant d'être bloqué.
 Donc ça veut dire que ce sémaphore S prod,
 le sémaphore de production, va être initié via N.
 C'est-à-dire, initialement, vous autorisez N production et zéro consommation au départ.
 Après, ça va changer.
 De même, pour éviter que deux processus qui accèdent simultanément à une même case,
 on va avoir un sémaphore d'exclusion mutuelle qui va nous permettre de protéger les cases du tampon.
 Donc c'est un sémaphore d'exclusion mutuelle classique, tel qu'on vient de le voir tout à l'heure,
 qui va être initialisé à 1 pour avoir un seul process à la fois sur une case du tampon.
 Donc le pseudo-code, vous verrez en détail en TD des producteurs consommateurs.
 On peut imaginer que le producteur, c'est une boucle infini.
 Il cherche en permanence à produire.
 Le consommateur, pareil, c'est une boucle infinie.
 Il cherche en permanence à consommer.
 Je peux avoir autant de producteurs et de consommateurs que je veux.
 Donc que va faire un producteur ?
 Là, il va produire un message.
 Donc il va construire un message.
 Une fois qu'il a construit son message, il va chercher à le déposer dans le tampon.
 Mais ça se trouve, le tampon est plein.
 Donc il va faire un P sur le sémaphore prod.
 Puis je produis.
 Donc P de S prod.
 Donc on voit bien que comme le sémaphore est initialisé à N,
 les N premiers à coup, je vais pouvoir produire.
 C'est le N + 1ème producteur qui va se bloquer.
 Donc le premier producteur va pouvoir y aller, le deuxième, le troisième, et ainsi de suite.
 Ils vont tous pouvoir produire jusqu'au Nème.
 Le N + 1ème sera bloqué parce que le sémaphore passera en négatif.
 Une fois que j'ai produit, je peux déposer dans une des cases du tampon.
 Donc cette partie-là, on la détaillera en TD.
 Donc vraiment le dépôt dans le tampon T, c'est ce qu'on détaillera en TD.
 Donc une fois que vous avez déposé, on est autorisé à produire.
 On sait qu'il y a une case disponible.
 On produit dans la case disponible.
 Une fois que j'ai produit, je dis "vas-y, consomme".
 J'autorise une consommation de plus.
 Je sais que je viens de déposer un message dans mon tampon.
 Ça veut dire qu'un des consommateurs peut y aller.
 Donc je fais "V" de "Esconde".
 "Vas-y, consomme".
 Puis je produis.
 "Vas-y, consomme".
 Le consommateur, la première chose qu'il fait avant de retirer une case du tampon,
 il faut qu'il sache s'il y a une case qui a été remplie.
 Il va faire "Puis-je, Esconde".
 On voit bien qu'au départ, mon consommateur va se bloquer.
 Le sémaphore "Esconde" s'est initialisé à zéro.
 Au départ, ils vont tous se bloquer.
 On va faire un petit exemple juste après.
 "Puis-je, Esconde".
 Ils vont tous se bloquer.
 Ils vont se bloquer jusqu'à ce qu'un des producteurs ait mis une case dans le tampon.
 À ce moment-là, un des consommateurs va être réveillé grâce au "V2, Esconde".
 Une fois qu'ils sont réveillés,
 lorsque j'arrive ici, vous êtes sûr qu'une case a été déposée.
 Pourquoi ?
 Parce que la seule chose qui peut débloquer un consommateur, c'est le "V2, Esconde".
 "V2, Esconde" s'est fait par le producteur une fois qu'il a déposé la case dans son tampon.
 Donc ici, lorsque j'arrive ici, je sais que quelque part, il y a une case disponible.
 Une case dans laquelle il y a des données.
 Donc je retire cette case,
 en faisant attention qu'on n'en ait pas deux, on peut retirer la même case.
 C'est pour ça que j'ai une contrainte de mutex.
 Une fois que j'ai vidé ma case, j'autorise une production supplémentaire.
 "Vas-y, de l'espreuve".
 J'essaie de l'illustrer sur un petit exemple.
 Imaginons que j'ai un système avec un tampon de taille 2.
 C'est-à-dire qu'il y a deux cases disponibles.
 Mon tampon, donc N=2.
 Et j'ai trois producteurs et trois consommateurs.
 Donc initialement, le sémaphore S_PROD est illustré en bleu à un compteur égal à 2,
 parce que j'ai deux cases disponibles, une file vide.
 Et le sémaphore S_COND, c'est illustré en rouge,
 à un compteur égal à 3, parce qu'initialement, le tampon est en vide, je ne peux pas consommer.
 Et une file vide.
 J'ai mes trois producteurs et mes trois consommateurs qui tentent en parallèle d'accéder au tampon.
 Qu'est-ce qui se passe déjà au niveau des consommateurs ?
 Chaque consommateur va faire "Puis-je consommer ?"
 Donc je vais faire un P de S_COND.
 C'est dans leur code.
 Le premier consommateur fait un P de S_COND.
 On décrémente la valeur du sémaphore S_COND, qui passe de 0 à -1.
 Dès que vous faites un P sur une valeur, et que le compteur passe en négatif,
 vous êtes bloqué dans la file du sémaphore.
 Le consommateur 1 va s'insérer dans la file du sémaphore,
 illustré ici par C1, et passe à l'état bloqué.
 Pareil pour le consommateur 2.
 Le compteur du sémaphore va passer de -1 à -2.
 Le consommateur 2 s'insère en que de la file du sémaphore.
 Et pareil pour le consommateur 3.
 Le compteur passe à -3, et le consommateur 3 s'insère dans la file du sémaphore.
 Au bout du compte, j'ai mes 3 consommateurs, C1, C2, C3,
 qui sont tous à état bloqué, insérés dans la file du sémaphore,
 et mon compteur va au -3.
 On observe bien que lorsque le compteur va au -3,
 ça veut dire que j'ai 3 processus bloqués dans la file d'attente.
 Donc là c'est cohérent, les 3 processus, tant que je n'ai rien à lire,
 ils se bloquent.
 Qu'est-ce qui se passe maintenant au niveau des producteurs ?
 Les producteurs peuvent produire, car au départ le tampon est vide,
 et j'ai 2 cases disponibles.
 Le premier producteur fait P2, puis je produis.
 On décrémente la valeur du sémaphore S_PROD.
 Le compteur passe de 2 à 1.
 Je ne suis pas négatif, je ne me bloque pas.
 Je commence à écrire dans la première case.
 En parallèle, le second producteur va être élu.
 Il tente également d'accéder au tampon.
 Il va faire Puis-je produire ?
 Je décrémente la valeur du compteur qui passe de 1 à 0.
 0 n'étant pas négatif, je ne peux plus produire.
 Il faudra juste faire attention, et c'est ce qu'on verra en TD,
 que chacun produit dans une case différente.
 Et là, chacun produit dans une case différente.
 Si maintenant un troisième producteur tente d'accéder au tampon,
 je n'ai plus de cases disponibles.
 Donc il faut qu'il se bloque.
 On va voir qu'il se bloque bien.
 En effet, il va faire Puis-je produire ?
 Je décrémente la valeur du compteur qui passe de 0 à -1.
 -1 est négatif.
 Donc le producteur 3 s'insère dans la fiche du sémaphore.
 Et parce qu'il a été bloqué.
 On voit bien que j'ai deux productions en parallèle.
 La troisième production est bloquée,
 et tous les consommateurs sont bloqués.
 C'est tout à fait cohérent.
 Lorsque les productions se sont terminées,
 une fois que j'ai bien rempli ma case,
 le producteur 1 a rempli sa case.
 Une fois qu'il a rempli sa case, il va faire V2S,
 vas-y consomme.
 A ce moment-là, c'est l'état courant de mon sémaphore S11.
 J'incrémente la valeur de mon compteur
 qui va passer de -3 à -2.
 Et je retire un des processus de la file d'attente.
 Ici, on va imaginer qu'on va retirer le premier C1.
 Voilà l'état de mon sémaphore maintenant,
 une fois que mon V2S11 est passé.
 Je n'ai plus que -2,
 je n'ai plus que C2 et C3 qui sont bloqués.
 Et le consommateur 1 est rebasculé.
 Donc le consommateur 1 va pouvoir lire
 ce qui a été déposé par le producteur 1.
 Et pareil, on continue.
 Lorsque le producteur 1...
 le producteur 2, pardon,
 a terminé de remplir sa case,
 il va faire un V2S11
 pour autoriser une consommation supplémentaire.
 Je décrémente mon compteur qui passe de -2 à -1
 et je réveille C2 en le retirant de la file d'attente.
 On voit bien là, j'ai mes deux productions en parallèle
 et j'ai mes deux consommations en parallèle.
 Il faut juste faire attention
 que chacun consomme une case différente.
 Ensuite, lorsque le premier consommateur
 a fini de retirer sa case,
 il a déclenché une des cases du tampon,
 c'est-à-dire qu'une des cases est disponible.
 À ce moment-là, on autorise une production supplémentaire
 en faisant le V2SPROD.
 Le V2SPROD, on reprend le stémaphore vert.
 Le compteur qui va à -1 maintenant vaut 0.
 Je retire un des processus de la file.
 Là, j'en ai qu'un seul, je n'ai pas le choix.
 Je retire P3 de la file et je rebascule à l'état près.
 Maintenant, le processus P3...
 le producteur 3 peut remplir la case
 qui vient d'être vidée par le consommateur 1.
 Une fois qu'il a terminé,
 une fois que le producteur 3
 a rempli la case,
 il va pouvoir réveiller le dernier consommateur
 en faisant un V2SCOND.
 À ce moment-là, j'incrémente mon compteur
 qui va à -1, mais passe à 0,
 et je réveille ces 3.
 Au bout du compte, j'ai bien eu les 3.
 J'ai eu 3 productions et bien 3 consommations.
 Et j'ai bien respecté mes contraintes.
 Maintenant, le dernier problème classique
 qu'on va voir, c'est le problème du lecteur-écrivain.
 C'est un autre problème de synchronisation
 qui est différent du producteur-consommateur.
 Ils se ressemblent un petit peu,
 mais vous allez voir qu'ils sont assez différents.
 Là, le plus simple, c'est d'imaginer
 qu'on a un ensemble de processus lecteur,
 vous ne savez pas combien,
 et un ensemble de processus écrivain
 qui partage un fichier.
 On a un fichier sur le disque, ici,
 un fichier bleu.
 Donc, eux écrivent, et eux cherchent à lire dessus.
 À chaque lecteur, ils exécutent le petit code suivant.
 Ouvrent lecture, ils font leur lecture,
 ferment lecture.
 Pareil pour les écrivains.
 Ils ouvrent écriture, ils écrivent,
 et ferment écriture.
 Le but du jeu, c'est de voir
 qu'est-ce qu'on met dans ouvre lecture,
 ferme lecture, ouvre écriture, ferme lecture.
 C'est là-dedans qu'on va synchroniser.
 Quelles sont nos contraintes, ici ?
 On veut que le fichier soit cohérent.
 Vous savez bien que si vous êtes plusieurs écrivains
 en même temps dans un fichier,
 vous risquez d'avoir des incohérences.
 De même, lorsque vous êtes en train de lire
 un fichier et quelqu'un écrit dessus,
 vous avez aussi une incohérence dans le fichier.
 Donc, la contrainte qu'on peut respecter, ici,
 c'est qu'on peut avoir soit un seul écrivain,
 si un écrivain qui a accès au fichier
 a basculement écrit tout seul,
 ou, c'est un "ou" exclusif,
 c'est pour ça que je l'ai représenté
 par le symbole "ou" exclusif,
 soit on a un seul écrivain,
 soit on a plusieurs lecteurs.
 Par contre, ce n'est pas gênant.
 On peut être plusieurs à lire en même temps un fichier.
 Là, il n'y a aucun souci de cohérence.
 Par contre, dès qu'il y a un écrivain,
 il doit être seul sur le fichier.
 C'est ça qu'on veut programmer avec des sémaphores.
 C'est un problème un petit peu plus compliqué
 que le producteur-consommateur.
 On ne va pas s'en sortir qu'avec des sémaphores.
 On va pouvoir poser une variable partagée supplémentaire.
 L'intuition qu'il y a derrière,
 c'est de se dire que je peux avoir autant de lecteurs que je veux.
 Par contre, quand j'ai des lecteurs,
 il ne faut pas que j'ai d'écrivains.
 L'astuce, c'est de se dire
 qu'on a envie que le premier lecteur,
 le premier qui accède au titan de lire le fichier,
 il verrouille le fichier en bloquant les écritures.
 Donc le premier lecteur va bloquer les écritures.
 On va avoir un jeton d'écriture unique
 qui va être pris par le premier lecteur.
 À ce moment-là, tous les lecteurs vont pouvoir avoir lieu en parallèle.
 Et dès que le dernier lecteur a fini de lire son fichier,
 il libère le jeton d'écriture.
 On voit bien que l'astuce,
 c'est de faire en sorte que le premier lecteur
 prenne le jeton d'écriture.
 Je vais avoir un P sur un jeton d'écriture unique.
 Et le dernier lecteur va faire un V sur ce jeton.
 La difficulté, c'est de savoir
 comment déterminer si je suis le premier ou le dernier lecteur.
 La façon la plus simple de gérer ça,
 c'est d'abord de définir une variable partagée
 qui va compter le nombre de lectures.
 Ça va me permettre de tester
 si je suis le premier lecteur ou le dernier.
 On va avoir une variable nbellec
 qui compte le nombre de lecteurs.
 Il y a même une variable partagée
 que tous les lecteurs vont incrémenter
 dès qu'ils vont tenter une lecture.
 Au départ, nbellec vaut 0,
 c'est-à-dire qu'il n'y a aucun lecteur.
 Et dès que nbellec vaut 1,
 ça veut dire qu'il y a un lecteur.
 Je suis le suite premier.
 Alors attention,
 dès que vous définissez une variable partagée,
 on ne peut pas être plusieurs en même temps
 et modifier le contenu d'une variable partagée.
 Pourquoi ? Parce qu'on risque d'avoir des incohérences,
 comme on a vu dans le cours précédent.
 Nbellec, c'est lui-même une ressource critique.
 Dès que vous définissez une variable partagée,
 et qu'il y a plusieurs processus
 qui peuvent manipuler la même variable partagée,
 il faut protéger cette variable par un sémaphore.
 On va avoir besoin d'un sémaphore MITEX
 qui va me protéger de nbellec,
 la variable nbellec qui compte le nombre de lecteurs.
 Donc là, il faut que ça devienne un réflexe.
 Dès que vous définissez une variable partagée,
 il faut la protéger par un MITEX,
 sinon vous aurez des incohérences.
 Ensuite, il me faut ce fameux sémaphore
 qui me bloque les écritures.
 Je vais avoir un sémaphore que je vais appeler E
 qui va me permettre de bloquer les écrits.
 Initialement, vous autorisez une seule écriture au départ.
 Ce sémaphore va être initialisé à 1.
 Il y a un seul jeton d'écriture.
 L'astuce est de faire que ce jeton d'écriture
 soit pris par le premier lecteur
 et libéré par le dernier lecteur.
 On va essayer d'implémenter ça.
 Ce qui est dur, c'est d'avoir cette idée-là.
 Le premier lecteur bloque,
 et le dernier lecteur débloque.
 Une fois qu'on a l'idée,
 c'est simple à implémenter avec des sémaphores,
 avec un petit peu d'habitude.
 Ici, on ouvre le lecteur.
 Qu'est-ce qui va se passer ?
 Là, j'ai mis en parallèle les 4 codes.
 Lorsque vous tentez une écriture,
 un écrivain arrive et va dire "Puis-je écrire ?"
 On voit bien que le premier écrivain,
 lorsqu'il arrive,
 il bloque les écritures.
 Il bloque les écritures.
 Je peux avoir qu'une seule écriture en même temps.
 Que va faire le lecteur ?
 Le lecteur va incrémenter la variable nbelect.
 Dès que vous touchez à une variable nbelect,
 il faut faire un "puis-je mutex".
 Il ne faut pas être plusieurs en même temps
 à modifier la variable nbelect.
 Vous ne savez pas le nombre de lecteurs.
 Vous pouvez avoir autant de lecteurs
 que vous n'en connaissez pas.
 Pour être sûr qu'on ne soit pas en même temps
 plusieurs à manipuler et à tester la variable nbelect,
 il faut mettre un mutex.
 Vous posez un mutex,
 parce que vous manipulez la variable nbelect.
 Vous incrémentez la variable nbelect.
 Si nbelect vaut 1, c'est que vous êtes le premier lecteur.
 À ce moment-là, vous faites "puis-je écrire ?"
 "Puis-je écrire ?" ça bloque les futures demandes d'écriture.
 Si vous êtes le premier lecteur qui arrive,
 vous faites un "puis-je écrire ?"
 Si un écrivain arrive après vous,
 il sera forcément bloqué dans "ouvre lecture".
 Vous piquez.
 Il y a un seul jeton d'écriture.
 Et le premier lecteur pique le jeton d'écriture.
 Ça veut dire que tous les écrivains qui arrivent après vous
 vont forcément se bloquer.
 Une fois qu'on a fait ça, on libère le semaphore.
 Ensuite, comment on fait ferme lecture ?
 Ferme lecture, c'est l'opération inverse.
 Lorsque vous faites ferme lecture,
 on décrémente toujours en mutex
 pour éviter les conflits d'accès à la même variable.
 Toujours en mutex, on décrémente le nombre de lecteurs.
 Si un bellec repasse à zéro, c'est qu'il y a eu le dernier lecteur.
 L'écrivain que vous avez bloqué en faisant le "p" d'écriture,
 vous allez le débloquer en faisant le "v" d'écriture.
 Ici, le "v" d'écriture va débloquer l'écrivain
 que vous avez bloqué ici, qui lui peut écrire.
 Il n'y a plus de lecture, donc il peut écrire.
 Une fois que l'on a fait ça, il ne faut pas oublier
 de libérer le mutex, sinon vous bloquez tout le monde.
 De même, l'écrivain va pouvoir faire son écriture,
 et lorsqu'il a terminé son écriture, il dit "c'est bon, je termine".
 Là, il fait un "v" d'écriture qui va débloquer
 soit un second écrivain, soit un lecteur qui était arrivé entre temps.
 Vous voyez bien que s'il y a une écriture en cours,
 vous faites "ouvre lecture", le premier lecteur qui va arriver
 va se bloquer ici.
 Lorsqu'il va faire PDE, s'il y a une écriture en cours, il va se bloquer.
 Donc là, ici, lorsque j'ai terminé mon écriture,
 il faut que je le débloque en faisant "v" d'écriture.
 Donc là, c'est une solution qui marche,
 et on verra en TD comment...
 C'est une solution qui marche, mais qui a un problème de famille.
 Notamment, on voit bien qu'à partir du moment
 où un lecteur est arrivé, il bloque les écritures.
 À ce moment-là, NB-Leg ferme la porte à l'écriture
 et tous les lecteurs peuvent arriver et lire.
 L'écrivain peut se faire doubler en permanence par les lecteurs.
 Donc ça marche, mais en termes de contraintes,
 on a bien les contraintes sous plusieurs lecteurs ou un écrivain.
 Par contre, il y a une risque de famille de l'écriture,
 et le but du sujet qu'on verra en TD, c'est de modifier ce petit code-là
 pour éviter les problèmes de famille, ce qu'on verra en TD.
 C'est une solution équitable.
 Pour finir, un autre point.
 Que ce soit le producteur-consommateur ou le lecteur-écrivain,
 on va les étudier en détail en TD.
 Un autre problème auquel je voudrais vous sensibiliser,
 c'est les problèmes d'inter-blocage,
 qui sont un problème assez embêtant lorsqu'on fait des synchronisations.
 C'est-à-dire que c'est très facile,
 vous le verrez, vous serez sans doute confronté à ça en TME,
 que de temps en temps votre système va complètement se bloquer.
 En fin de compte, c'est très facile de générer
 ce qu'on appelle des inter-blocages, des attentes mutuelles.
 Typiquement, imaginons qu'un process P1 attend une ressource détenue par P2.
 P2 attend une ressource détenue par P3, et ainsi de suite,
 jusqu'à ce qu'un process PN attende une ressource détenue par P1.
 Là, on a une attente cyclique. P1 attend P2, P2 attend P3, P3, et ainsi de suite,
 et PN attend P1.
 Et ainsi de suite, ça tente mutuellement.
 À ce moment-là, votre système est complètement gelé.
 Chacun s'attend à une attente mutuelle,
 ce qu'on appelle un verrou mortel, deadlock en anglais,
 ou inter-blocage en français.
 C'est très facile de générer un inter-blocage si on ne fait pas attention.
 Imaginons par exemple que j'ai deux variables partagées, A et B,
 que je manipule. Ce sont des variables partagées.
 Le réflexe que je vous ai dit tout à l'heure,
 dès que vous avez une variable partagée, il faut un sémaphore mutex
 pour protéger cette variable.
 On ne peut pas, lorsqu'on a plusieurs, en même temps modifier
 la même variable en mémoire.
 De manière naturelle, je définis un sémaphore mutex 1 pour protéger la variable A,
 et un sémaphore mutex 2 pour protéger la variable B.
 Ce sont deux variables différentes, il n'y a pas de raison d'avoir le même mutex pour les deux.
 Pendant que je manipule A, je peux manipuler B.
 C'est pour ça que j'ai deux mutex différents.
 Imaginons que je m'en procuse P1, qui lui fait l'opération
 A = A + B,
 et P2 qui fait l'opération
 B = B + A.
 Donc P1, de manière naturelle, on dit
 je manipule A puis B, donc je pose un sémaphore,
 je verrouille la variable A,
 puis la variable B,
 P mutex 1 et P mutex B.
 Ensuite je manipule mes deux variables,
 et lorsque j'ai fini de manipuler mes deux variables,
 je libère l'accès à ma variable B,
 puis je libère l'accès à ma variable A.
 P2 fait la même chose.
 Mais comme il manipule d'abord B puis A,
 il dit je pose le verrou sur B puis A,
 et enfin je le verrouille sur A puis B.
 Et il fait son opération entre les deux.
 Donc là ici, dès que vous voyez quelque chose comme ça,
 attention, on a tendance à croiser.
 Donc là c'est facile de voir que dans cette exécution-là,
 il y a des scénarios où tout va très bien se passer.
 Si j'ai P1 qui est élu d'un coup, puis P2,
 tout va bien se passer.
 C'est très facile de mettre en défaut cette exécution-là.
 Ici par exemple, vous avez P1 qui commence à s'exécuter,
 il fait son P_Mutex1,
 donc lui il peut passer.
 Mais entre-temps, P2 passe.
 Ici, entre le P_Mutex1 et P_Mutex2,
 vous avez une commutation et P2 est élu.
 P2 va prendre le sémaphore Mutex2,
 donc lui va pouvoir passer,
 et il va se bloquer sur Mutex1.
 Donc P2 va bloquer ici,
 en faisant le P_Mutex1, je bloque celui-là,
 en faisant le P_Mutex2, je bloque celui-là.
 Donc les deux processus se bloquent mutuellement,
 et là votre système est complètement gelé.
 La seule chose que vous pouvez faire,
 c'est de faire un ctrl+C,
 tuer les processus et éventuellement les relancer,
 mais là vous avez un bug dans votre synchronisation.
 Donc le réflexe,
 c'est un problème classique dans les bases de données.
 Quand vous avez plusieurs données que vous manipulez,
 il faut faire très attention
 dans quel ordre on pose les verrous sur les données.
 Un moyen très simple d'éviter ce genre de problème ici,
 c'est de toujours verrouiller les données dans le même ordre.
 Donc on ne fait jamais P_Mutex1, P_Mutex2, P_Mutex2, ou P_Mutex1.
 Il faut toujours les verrouiller dans l'ordre 1, 2 par exemple.
 Donc si c'est un ordre arbitraire,
 je verrouille toujours dans le même ordre les ressources.
 Je pose d'abord le verrou sur A, puis B, et jamais B, puis A.
 Donc si vous inversez ces deux lignes-là,
 vous avez résolu votre problème d'exécution mutuelle.
 Il suffit d'analyser ce problème,
 et à ce moment-là vous n'aurez plus ce problème d'exécution mutuelle.
 Des fois c'est plus subtil à résoudre,
 mais il faut être vigilant à ça.
 Donc l'ordre dans lequel vous faites vos P est très important.
 Et si vous ne faites pas attention,
 vous pouvez avoir des problèmes d'interblocage.
 On y sera aussi quelques problèmes d'interblocage plus subtils.
 Donc voilà, là on a fait le tour sur les problèmes de synchronisation.
 À partir des prochains cours,
 on va commencer à aborder les problèmes liés à la mémoire.
 Je vous remercie.
